﻿using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

namespace GameToolkit
{
    public enum EShaderPropertyType
    {
        Float,
        Vector,
        Color,
        HdrColor,
    }

    [CreateAssetMenu(fileName = "MatInstance", menuName = "Material Instance", order = 300)]
    public class MaterialInstance : ScriptableObject
    {
#pragma warning disable CS0649

        //[SerializeField, HideInInspector]
        //private Shader m_Shader;

        [SerializeField, HideInInspector]
        private string m_ShaderName;

        [SerializeField, HideInInspector]
        private Property[] m_Properties;

        MaterialPropertyBlock mSharedBlock;
        public MaterialPropertyBlock sharedPropertyBlock
        {
            get
            {
                if (mSharedBlock == null)
                {
                    mSharedBlock = new MaterialPropertyBlock();
                    SetMaterial(mSharedBlock);
                }
                return mSharedBlock;
            }
        }

#pragma warning restore

        public event System.Action OnValidateMaterial;
        public int PropertyCount { get { return m_Properties == null ? 0 : m_Properties.Length; } }

        public Property this[int index]
        {
            get { return m_Properties[index]; }
            set
            {
                if (!m_Properties[index].Equals(value))
                {
                    m_Properties[index] = value;
#if UNITY_EDITOR
                    EditorUtility.SetDirty(this);
#endif
                    OnValidateMaterial?.Invoke();
                }
            }
        }

        public int IndexOfProperty(string name)
        {
            var len = m_Properties == null ? 0 : m_Properties.Length;
            for (int i = 0; i < len; i++)
            {
                if (m_Properties[i].name == name)
                    return i;
            }
            return -1;
        }

        public int IndexOfProperty(int propertyId)
        {
            var len = m_Properties == null ? 0 : m_Properties.Length;
            for (int i = 0; i < len; i++)
            {
                if (Shader.PropertyToID(m_Properties[i].name) == propertyId)
                    return i;
            }
            return -1;
        }

        public void SetFloat(int index, float value)
        {
            var p = m_Properties[index];
            var v = p.value;
            if(!Mathf.Approximately( v.x, value))
            {
                v.x = value;
                m_Properties[index] = p;
                OnValidateMaterial?.Invoke();
            }
        }

        public void SetAlpha(int index, float value)
        {
            var p = m_Properties[index];
            var v = p.value;
            if(!Mathf.Approximately(v.w, value))
            {
                v.w = value;
                m_Properties[index] = p;
                OnValidateMaterial?.Invoke();
            }
        }

        public void SetVector(int index, Vector4 value)
        {
            var p = m_Properties[index];
            var v = p.value;
            if (!Mathf.Approximately(0, Vector4.SqrMagnitude(v - value)))
            {
                p.value = v;
                m_Properties[index] = p;
                OnValidateMaterial?.Invoke();
            }
        }

        public void SetColor(int index, Color color)
        {
            SetVector(index, color);
        }

        public void SetMaterial(IMaterialPropertyBlock mat)
        {
            var len = mat == null || m_Properties == null ? 0 : m_Properties.Length;
            for (int i = 0; i < len; i++)
            {
                SetValue(mat, m_Properties[i]);
            }
        }

        public void SetMaterial(Material mat)
        {
            var inst = MaterialCache.MaterialInst.sharingInst;
            lock (inst)
            {
                inst._material = mat;
                SetMaterial(inst);
                inst._material = null;
            }
        }

        public void SetMaterial(MaterialPropertyBlock block)
        {
            var inst = OverrideMPB.BlockInst.sharingInst;
            lock (inst)
            {
                inst.block = block;
                SetMaterial(inst);
                inst.block = null;
            }
        }

        private void OnDestroy()
        {
            OnValidateMaterial = null;
        }

#if UNITY_EDITOR
        private void OnValidate()
        {
            if(mSharedBlock != null)
            {
                SetMaterial(mSharedBlock);
            }
            OnValidateMaterial?.Invoke();
        }
#endif

        [System.Serializable]
        public struct Property :System.IEquatable<Property>
        {
            public string name;
            public Vector4 value;
            public EShaderPropertyType type;
            public int id
            {
                get
                {
                    return Shader.PropertyToID(name);
                }
            }

            public float FloatValue
            {
                get { return value.x; }
                set
                {
                    var v = this.value;
                    v.x = value;
                    this.value = v;
                }
            }

            public int IntegerValue
            {
                get { return (int)value.x; }
                set
                {
                    var v = this.value;
                    v.x = value;
                    this.value = v;
                }
            }

            public Property(string pname, EShaderPropertyType type)
            {
                name = pname;
                this.type = type;
                this.value = Vector4.zero;
            }

            public bool Equals(Property other)
            {
                return type == other.type && Mathf.Approximately(0, Vector4.SqrMagnitude(value - other.value)) && this.name == other.name;
            }
        }

        public static void SetValue(IMaterialPropertyBlock block, Property p)
        {
            if (p.type == EShaderPropertyType.Float)
                block.SetFloat(p.name, p.value.x);
            else if (p.type == EShaderPropertyType.Vector)
                block.SetVector(p.name, p.value);
            else if(p.type == EShaderPropertyType.Color || p.type == EShaderPropertyType.HdrColor)
                block.SetColor(p.name, p.value);
        }

        public static void SetValue(MaterialPropertyBlock block, Property p)
        {
            var inst = OverrideMPB.BlockInst.sharingInst;
            lock (inst)
            {
                inst.block = block;
                SetValue(inst, p);
                inst.block = null;
            }
        }

        public static void SetValue(Material mat, Property p)
        {
            var inst = MaterialCache.MaterialInst.sharingInst;
            lock (inst)
            {
                inst._material = mat;
                SetValue(inst, p);
                inst._material = null;
            }
        }

        public static bool Blend(IMaterialPropertyBlock mat, MaterialInstance instance1, MaterialInstance instance2, float weight, bool excludeNotSame = true)
        {
            var dirty = false;
            if(instance1 == instance2)
            {
                dirty = instance1 != null && instance1.PropertyCount > 0;
                if (dirty)
                    instance1.SetMaterial(mat);
                return dirty;
            }
            var sizea = instance1 == null || instance1.m_Properties == null ? 0 : instance1.m_Properties.Length;
            var sizeb = instance2 == null || instance2.m_Properties == null ? 0 : instance2.m_Properties.Length;
           
            cache.Clear();
            MaterialInstance lsta, lstb;
            if (sizea >= sizeb)
            {
                lsta = instance1;
                lstb = instance2;
            }
            else
            {
                lsta = instance2;
                lstb = instance1;
                weight = 1 - weight;
                sizea = sizeb;
                sizeb = lstb == null || lstb.m_Properties == null ? 0 : lstb.m_Properties.Length;
            }
            for (int i = 0; i < sizea; i++)
            {
                var va = lsta.m_Properties[i];
                var n = lstb == null ? -1 : lstb.IndexOfProperty(va.name);// IndexOfProperty(lstb, va.id);
                if (n != -1)
                {
                    cache.Add(va.name);
                    va.value = Vector4.Lerp(va.value, lstb.m_Properties[n].value, weight);
                    SetValue(mat, va);
                    dirty = true;
                }
                else if (!excludeNotSame)
                {
                    SetValue(mat, va);
                    dirty = true;
                }
            }
            if (!excludeNotSame && sizeb > 0)
            {
                for (int i = 0; i < sizeb; i++)
                {
                    var vb = lstb.m_Properties[i];
                    if (!cache.Contains(vb.name))
                    {
                        SetValue(mat, vb);
                        dirty = true;
                    }
                }
            }
            return dirty;
        }

        public static bool Blend(Material mat, MaterialInstance instance1, MaterialInstance instance2, float weight, bool excludeNotSame = true)
        {
            if (mat == null)
                return false;
            var inst = MaterialCache.MaterialInst.sharingInst;
            lock (inst)
            {
                inst._material = mat;
                var dirty = Blend(inst, instance1, instance2, weight, excludeNotSame);
                inst._material = null;
                return dirty;
            }
        }

        public static bool Blend(MaterialPropertyBlock block, MaterialInstance instance1, MaterialInstance instance2, float weight, bool excludeNotSame = true)
        {
            if (block == null)
                return false;
            var inst = OverrideMPB.BlockInst.sharingInst;
            lock (inst)
            {
                inst.block = block;
                var dirty = Blend(inst, instance1, instance2, weight, excludeNotSame);
                inst.block = null;
                return dirty;
            }
        }

        public static void CopyMaterialProperties(Material src, Material dest)
        {
            var srcshader = src.shader;
            if (srcshader == null)
                return;
            var len = srcshader.GetPropertyCount();
            for (int i = 0; i < len; i++)
            {
                var pid = srcshader.GetPropertyNameId(i);
                if (!dest.HasProperty(pid))
                    continue;
                var tp = srcshader.GetPropertyType(i);
                if (tp == UnityEngine.Rendering.ShaderPropertyType.Texture)
                {
                    var tex = src.GetTexture(pid);
                    if (tex != null)
                        dest.SetTexture(pid, tex);
                }
                else if (tp == UnityEngine.Rendering.ShaderPropertyType.Color || tp == UnityEngine.Rendering.ShaderPropertyType.Vector)
                {
                    dest.SetVector(pid, src.GetVector(pid));
                }
                else if (tp == UnityEngine.Rendering.ShaderPropertyType.Float || tp == UnityEngine.Rendering.ShaderPropertyType.Range)
                {
                    dest.SetFloat(pid, src.GetFloat(pid));
                }
            }
        }

        Shader mShader;
        public Shader shader
        {
            get
            {
                if (mShader == null || mShader.name != m_ShaderName)
                {
                    mShader = string.IsNullOrEmpty(m_ShaderName) ? null : Shader.Find(m_ShaderName);
                }
                return mShader;
            }
            set
            {
                if (mShader != value)
                {
                    mShader = value;
                    m_ShaderName = value == null ? "" : value.name;
#if UNITY_EDITOR
                    EditorUtility.SetDirty(this);
#endif
                }
            }
        }

        static HashSet<string> cache = new HashSet<string>();
    }
}